all repos — weimar @ a8921d226b7ab5b6d366d88c51d3308362388cf7

Unnamed repository; edit this file 'description' to name the repository.

web/src/routes/image/[id]/+page.svelte (view raw)

  1<script lang="ts">
  2	import { onMount } from 'svelte';
  3	import { getImage, imageDownloadUrl, thumbnailUrl, type ImageDetail } from '$lib/api';
  4
  5	let { params } = $props();
  6	let id = $derived(parseInt(params.id));
  7
  8	let img = $state<ImageDetail | null>(null);
  9	let loading = $state(true);
 10	let error = $state('');
 11
 12	onMount(async () => {
 13		try {
 14			img = await getImage(id);
 15		} catch (err) {
 16			error = err instanceof Error ? err.message : 'Failed to load image';
 17		} finally {
 18			loading = false;
 19		}
 20	});
 21
 22	function formatSize(bytes: number): string {
 23		if (bytes < 1024) return bytes + ' B';
 24		if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
 25		return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
 26	}
 27
 28	function formatDate(s: string): string {
 29		return new Date(s).toLocaleString();
 30	}
 31</script>
 32
 33{#if loading}
 34	<p class="state-msg">Loading...</p>
 35{:else if error}
 36	<p class="state-msg error">{error}</p>
 37{:else if img}
 38	<div class="page-head">
 39		<h1>{img.filename}</h1>
 40		<div class="head-line"></div>
 41	</div>
 42
 43	<div class="layout">
 44		<div class="preview">
 45			{#if img.thumbnail_path}
 46				<img src={imageDownloadUrl(img.id)} alt={img.filename} />
 47			{:else}
 48				<div class="no-thumb">NO THUMBNAIL</div>
 49			{/if}
 50		</div>
 51
 52		<div class="meta">
 53			<div class="meta-block">
 54				<span class="meta-label">DIMENSIONS</span>
 55				<span class="meta-value">{img.width} &times; {img.height}</span>
 56			</div>
 57			<div class="meta-block">
 58				<span class="meta-label">FILE SIZE</span>
 59				<span class="meta-value">{formatSize(img.file_size)}</span>
 60			</div>
 61			<div class="meta-block">
 62				<span class="meta-label">TYPE</span>
 63				<span class="meta-value">{img.mime_type}</span>
 64			</div>
 65			<div class="meta-block">
 66				<span class="meta-label">UPLOADED</span>
 67				<span class="meta-value">{formatDate(img.created_at)}</span>
 68			</div>
 69			<div class="meta-block tags-block">
 70				<span class="meta-label">TAGS</span>
 71				<div class="tag-pills">
 72					{#if img.tags_detail.length > 0}
 73						{#each img.tags_detail as t (t.tag)}
 74							<a href="/browse?tag={encodeURIComponent(t.tag)}" class="tag-pill" title="Used {t.usage_count} time{t.usage_count === 1 ? '' : 's'}">
 75								<span class="tag-name">{t.tag}</span>
 76								<span class="tag-count">{t.usage_count}</span>
 77							</a>
 78						{/each}
 79					{:else}
 80						<span class="none">none</span>
 81					{/if}
 82				</div>
 83			</div>
 84
 85			<a href={imageDownloadUrl(img.id)} class="btn" download={img.filename}>
 86				DOWNLOAD ORIGINAL
 87			</a>
 88		</div>
 89	</div>
 90
 91	<p class="back"><a href="/browse">&larr; BACK TO BROWSE</a></p>
 92{/if}
 93
 94<style>
 95	.state-msg {
 96		padding: 2rem 0;
 97		font-size: 1.1rem;
 98		color: #888;
 99	}
100	.error {
101		color: #c62828;
102	}
103
104	.page-head {
105		margin-bottom: 1.5rem;
106	}
107	.page-head h1 {
108		word-break: break-all;
109	}
110	.head-line {
111		height: 3px;
112		background: #c62828;
113		width: 3rem;
114		margin-top: 0.4rem;
115	}
116
117	.layout {
118		display: flex;
119		gap: 2rem;
120		flex-wrap: wrap;
121	}
122	.preview {
123		flex: 1;
124		min-width: 300px;
125		background: #1a1a1a;
126		display: flex;
127		align-items: center;
128		justify-content: center;
129	}
130	.preview img {
131		display: block;
132		width: 100%;
133		height: auto;
134		max-height: 70vh;
135		object-fit: contain;
136	}
137	.no-thumb {
138		padding: 4rem 2rem;
139		color: #555;
140		font-family: 'Oswald', sans-serif;
141		font-weight: 500;
142		font-size: 0.9rem;
143		letter-spacing: 0.12em;
144	}
145
146	.meta {
147		width: 260px;
148		flex-shrink: 0;
149		display: flex;
150		flex-direction: column;
151		gap: 1rem;
152	}
153	.meta-block {
154		display: flex;
155		flex-direction: column;
156		gap: 0.15rem;
157	}
158	.meta-label {
159		font-family: 'Oswald', sans-serif;
160		font-weight: 500;
161		font-size: 0.7rem;
162		letter-spacing: 0.15em;
163		color: #999;
164	}
165	.meta-value {
166		font-family: 'Inter', sans-serif;
167		font-size: 0.9rem;
168		font-weight: 600;
169		color: #1a1a1a;
170	}
171
172	.tags-block {
173		flex: 1;
174	}
175	.tag-pills {
176		display: flex;
177		flex-wrap: wrap;
178		gap: 0.35rem;
179	}
180	.tag-pill {
181		display: inline-flex;
182		align-items: center;
183		gap: 0.3rem;
184		background: #1a1a1a;
185		padding: 0.25rem 0.5rem 0.25rem 0.65rem;
186		font-family: 'Inter', sans-serif;
187		font-size: 0.78rem;
188		font-weight: 500;
189		color: #eee;
190		text-decoration: none;
191		transition: background 0.15s;
192	}
193	.tag-pill:hover {
194		background: #c62828;
195		text-decoration: none;
196	}
197	.tag-name {
198		font-weight: 600;
199	}
200	.tag-count {
201		background: rgba(255, 255, 255, 0.12);
202		padding: 0.05rem 0.35rem;
203		font-size: 0.68rem;
204		font-weight: 600;
205		color: rgba(255, 255, 255, 0.65);
206		min-width: 1.1rem;
207		text-align: center;
208	}
209	.tag-pill:hover .tag-count {
210		background: rgba(0, 0, 0, 0.2);
211	}
212	.none {
213		color: #bbb;
214		font-size: 0.85rem;
215	}
216
217	.btn {
218		display: inline-flex;
219		align-items: center;
220		align-self: flex-start;
221		padding: 0.55rem 1.25rem;
222		background: #c62828;
223		color: #fff;
224		border: none;
225		font-family: 'Oswald', sans-serif;
226		font-weight: 500;
227		font-size: 0.8rem;
228		letter-spacing: 0.1em;
229		text-decoration: none;
230		text-transform: uppercase;
231		cursor: pointer;
232		transition: background 0.15s;
233		margin-top: 0.5rem;
234	}
235	.btn:hover {
236		background: #b71c1c;
237		text-decoration: none;
238	}
239
240	.back {
241		margin-top: 2rem;
242	}
243	.back a {
244		font-family: 'Oswald', sans-serif;
245		font-weight: 500;
246		font-size: 0.85rem;
247		letter-spacing: 0.1em;
248	}
249</style>